mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-04 11:36:44 +00:00
fix: read prompt subcommand input from stdin
This commit is contained in:
@@ -1399,10 +1399,37 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
}
|
||||
"export" => parse_export_args(&rest[1..], output_format),
|
||||
"prompt" => {
|
||||
let prompt = rest[1..].join(" ");
|
||||
let mut read_stdin = false;
|
||||
let prompt_parts = rest[1..]
|
||||
.iter()
|
||||
.filter_map(|arg| {
|
||||
if matches!(arg.as_str(), "--stdin" | "--prompt-stdin") {
|
||||
read_stdin = true;
|
||||
None
|
||||
} else {
|
||||
Some(arg.as_str())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let positional_prompt = prompt_parts.join(" ");
|
||||
let stdin_prompt = if read_stdin || positional_prompt.trim().is_empty() {
|
||||
read_piped_stdin()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prompt = if read_stdin {
|
||||
merge_prompt_with_stdin(&positional_prompt, stdin_prompt.as_deref())
|
||||
} else {
|
||||
stdin_prompt
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or(&positional_prompt)
|
||||
.to_string()
|
||||
};
|
||||
if prompt.trim().is_empty() {
|
||||
// #750: provide error_kind-compatible prefix + \n for hint extraction
|
||||
return Err("missing_prompt: prompt subcommand requires a prompt string.\nUsage: claw prompt <text> or echo '<text>' | claw".to_string());
|
||||
// #750/#823/#423: provide error_kind-compatible prefix + \n for hint extraction.
|
||||
return Err("missing_prompt: prompt subcommand requires a prompt string.
|
||||
Usage: claw prompt <text> or echo '<text>' | claw prompt".to_string());
|
||||
}
|
||||
Ok(CliAction::Prompt {
|
||||
prompt,
|
||||
@@ -11608,9 +11635,12 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " Start the interactive REPL")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw [--model MODEL] [--output-format text|json] prompt TEXT"
|
||||
" claw [--model MODEL] [--output-format text|json] prompt [--stdin] [TEXT]"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" Send one prompt and exit; reads stdin when TEXT is omitted"
|
||||
)?;
|
||||
writeln!(out, " Send one prompt and exit")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw [--model MODEL] [--output-format text|json] TEXT"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::while_let_on_iterator)]
|
||||
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -245,6 +246,119 @@ stderr:
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_subcommand_reads_prompt_from_stdin_when_no_positional_arg_423() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
let base_url = server.base_url();
|
||||
|
||||
let workspace = unique_temp_dir("prompt-stdin-423");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
|
||||
let prompt = format!("{SCENARIO_PREFIX}streaming_text\n");
|
||||
let output = run_claw_with_stdin(
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&base_url,
|
||||
&[
|
||||
"prompt",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--compact",
|
||||
"--permission-mode",
|
||||
"read-only",
|
||||
"--model",
|
||||
"sonnet",
|
||||
],
|
||||
&prompt,
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"prompt stdin run should succeed\nstdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let parsed: Value = serde_json::from_slice(&output.stdout).expect("stdout should parse");
|
||||
assert_eq!(
|
||||
parsed["message"],
|
||||
"Mock streaming says hello from the parity harness."
|
||||
);
|
||||
let captured = runtime.block_on(server.captured_requests());
|
||||
assert!(
|
||||
captured
|
||||
.iter()
|
||||
.any(|request| request.raw_body.contains("PARITY_SCENARIO:streaming_text")),
|
||||
"stdin prompt should reach the provider request: {captured:?}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_subcommand_stdin_flag_appends_pipe_context_423() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
let base_url = server.base_url();
|
||||
|
||||
let workspace = unique_temp_dir("prompt-stdin-flag-423");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
|
||||
let prompt_context = format!("{SCENARIO_PREFIX}streaming_text\n");
|
||||
let output = run_claw_with_stdin(
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&base_url,
|
||||
&[
|
||||
"prompt",
|
||||
"Use stdin context",
|
||||
"--stdin",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--compact",
|
||||
"--permission-mode",
|
||||
"read-only",
|
||||
"--model",
|
||||
"sonnet",
|
||||
],
|
||||
&prompt_context,
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"prompt --stdin run should succeed\nstdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let captured = runtime.block_on(server.captured_requests());
|
||||
let provider_body = captured
|
||||
.iter()
|
||||
.find(|request| request.raw_body.contains("Use stdin context"))
|
||||
.expect("merged prompt should reach provider");
|
||||
assert!(
|
||||
provider_body
|
||||
.raw_body
|
||||
.contains("PARITY_SCENARIO:streaming_text"),
|
||||
"merged prompt should include stdin context: {provider_body:?}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_subcommand_json_help_fails_fast_when_stdin_closed() {
|
||||
let workspace = unique_temp_dir("compact-nontty-json-help");
|
||||
@@ -356,6 +470,39 @@ fn run_claw(
|
||||
command.output().expect("claw should launch")
|
||||
}
|
||||
|
||||
fn run_claw_with_stdin(
|
||||
cwd: &std::path::Path,
|
||||
config_home: &std::path::Path,
|
||||
home: &std::path::Path,
|
||||
base_url: &str,
|
||||
args: &[&str],
|
||||
stdin: &str,
|
||||
) -> Output {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_claw"))
|
||||
.current_dir(cwd)
|
||||
.env_clear()
|
||||
.env("ANTHROPIC_API_KEY", "test-compact-key")
|
||||
.env("ANTHROPIC_BASE_URL", base_url)
|
||||
.env("CLAW_CONFIG_HOME", config_home)
|
||||
.env("HOME", home)
|
||||
.env("NO_COLOR", "1")
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.args(args)
|
||||
.spawn()
|
||||
.expect("claw should launch");
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.expect("stdin should be piped")
|
||||
.write_all(stdin.as_bytes())
|
||||
.expect("stdin should write");
|
||||
child.stdin.take();
|
||||
child.wait_with_output().expect("output should collect")
|
||||
}
|
||||
|
||||
fn run_claw_closed_stdin_with_timeout(
|
||||
cwd: &std::path::Path,
|
||||
config_home: &std::path::Path,
|
||||
|
||||
Reference in New Issue
Block a user