summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEd Zynda <[email protected]>2025-05-23 14:54:09 +0300
committerGitHub <[email protected]>2025-05-23 06:54:09 -0500
commit89e3a72ae10b96cc1d8a01a8882c6d9e81f20b6a (patch)
tree1057cf90f0c55556de740fdb6798464e88732839
parentb9ebcea82c262dc834633c2c8f44a94fe8773a15 (diff)
downloadopencode-89e3a72ae10b96cc1d8a01a8882c6d9e81f20b6a.tar.gz
opencode-89e3a72ae10b96cc1d8a01a8882c6d9e81f20b6a.zip
feat: add support for piped input to CLI (#51)
-rw-r--r--README.md15
-rw-r--r--cmd/root.go30
-rw-r--r--cmd/root_test.go143
3 files changed, 187 insertions, 1 deletions
diff --git a/README.md b/README.md
index b4ed0f7f0..c9ebb45e7 100644
--- a/README.md
+++ b/README.md
@@ -245,26 +245,39 @@ opencode -c /path/to/project
## Non-interactive Prompt Mode
-You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
+You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument or by piping text into the command. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
```bash
# Run a single prompt and print the AI's response to the terminal
opencode -p "Explain the use of context in Go"
+# Pipe input to OpenCode (equivalent to using -p flag)
+echo "Explain the use of context in Go" | opencode
+
# Get response in JSON format
opencode -p "Explain the use of context in Go" -f json
+# Or with piped input
+echo "Explain the use of context in Go" | opencode -f json
# Run without showing the spinner
opencode -p "Explain the use of context in Go" -q
+# Or with piped input
+echo "Explain the use of context in Go" | opencode -q
# Enable verbose logging to stderr
opencode -p "Explain the use of context in Go" --verbose
+# Or with piped input
+echo "Explain the use of context in Go" | opencode --verbose
# Restrict the agent to only use specific tools
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
+# Or with piped input
+echo "Explain the use of context in Go" | opencode --allowedTools=view,ls,glob
# Prevent the agent from using specific tools
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
+# Or with piped input
+echo "Explain the use of context in Go" | opencode --excludedTools=bash,edit
```
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session.
diff --git a/cmd/root.go b/cmd/root.go
index 39d58eab6..ab102afe6 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
+ "io"
"os"
"sync"
"time"
@@ -91,6 +92,16 @@ to assist developers in writing, debugging, and understanding code directly from
// Check if we're in non-interactive mode
prompt, _ := cmd.Flags().GetString("prompt")
+
+ // Check for piped input if no prompt was provided via flag
+ if prompt == "" {
+ pipedInput, hasPipedInput := checkStdinPipe()
+ if hasPipedInput {
+ prompt = pipedInput
+ }
+ }
+
+ // If we have a prompt (either from flag or piped input), run in non-interactive mode
if prompt != "" {
outputFormatStr, _ := cmd.Flags().GetString("output-format")
outputFormat := format.OutputFormat(outputFormatStr)
@@ -311,6 +322,25 @@ func Execute() {
}
}
+// checkStdinPipe checks if there's data being piped into stdin
+func checkStdinPipe() (string, bool) {
+ // Check if stdin is not a terminal (i.e., it's being piped)
+ stat, _ := os.Stdin.Stat()
+ if (stat.Mode() & os.ModeCharDevice) == 0 {
+ // Read all data from stdin
+ data, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return "", false
+ }
+
+ // If we got data, return it
+ if len(data) > 0 {
+ return string(data), true
+ }
+ }
+ return "", false
+}
+
func init() {
rootCmd.Flags().BoolP("help", "h", false, "Help")
rootCmd.Flags().BoolP("version", "v", false, "Version")
diff --git a/cmd/root_test.go b/cmd/root_test.go
new file mode 100644
index 000000000..284ef5837
--- /dev/null
+++ b/cmd/root_test.go
@@ -0,0 +1,143 @@
+package cmd
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "testing"
+)
+
+func TestCheckStdinPipe(t *testing.T) {
+ // Save original stdin
+ origStdin := os.Stdin
+
+ // Restore original stdin when test completes
+ defer func() {
+ os.Stdin = origStdin
+ }()
+
+ // Test case 1: Data is piped in
+ t.Run("WithPipedData", func(t *testing.T) {
+ // Create a pipe
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("Failed to create pipe: %v", err)
+ }
+
+ // Replace stdin with our pipe
+ os.Stdin = r
+
+ // Write test data to the pipe
+ testData := "test piped input"
+ go func() {
+ defer w.Close()
+ w.Write([]byte(testData))
+ }()
+
+ // Call the function
+ data, hasPiped := checkStdinPipe()
+
+ // Check results
+ if !hasPiped {
+ t.Error("Expected hasPiped to be true, got false")
+ }
+ if data != testData {
+ t.Errorf("Expected data to be %q, got %q", testData, data)
+ }
+ })
+
+ // Test case 2: No data is piped in (simulated terminal)
+ t.Run("WithoutPipedData", func(t *testing.T) {
+ // Create a temporary file to simulate a terminal
+ tmpFile, err := os.CreateTemp("", "terminal-sim")
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %v", err)
+ }
+ defer os.Remove(tmpFile.Name())
+ defer tmpFile.Close()
+
+ // Open the file for reading
+ f, err := os.Open(tmpFile.Name())
+ if err != nil {
+ t.Fatalf("Failed to open temp file: %v", err)
+ }
+ defer f.Close()
+
+ // Replace stdin with our file
+ os.Stdin = f
+
+ // Call the function
+ data, hasPiped := checkStdinPipe()
+
+ // Check results
+ if hasPiped {
+ t.Error("Expected hasPiped to be false, got true")
+ }
+ if data != "" {
+ t.Errorf("Expected data to be empty, got %q", data)
+ }
+ })
+}
+
+// This is a mock implementation for testing since we can't easily mock os.Stdin.Stat()
+// in a way that would return the correct Mode() for our test cases
+func mockCheckStdinPipe(reader io.Reader, isPipe bool) (string, bool) {
+ if !isPipe {
+ return "", false
+ }
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ return "", false
+ }
+
+ if len(data) > 0 {
+ return string(data), true
+ }
+ return "", false
+}
+
+func TestMockCheckStdinPipe(t *testing.T) {
+ // Test with data
+ t.Run("WithData", func(t *testing.T) {
+ testData := "test data"
+ reader := bytes.NewBufferString(testData)
+
+ data, hasPiped := mockCheckStdinPipe(reader, true)
+
+ if !hasPiped {
+ t.Error("Expected hasPiped to be true, got false")
+ }
+ if data != testData {
+ t.Errorf("Expected data to be %q, got %q", testData, data)
+ }
+ })
+
+ // Test without data
+ t.Run("WithoutData", func(t *testing.T) {
+ reader := bytes.NewBufferString("")
+
+ data, hasPiped := mockCheckStdinPipe(reader, true)
+
+ if hasPiped {
+ t.Error("Expected hasPiped to be false, got true")
+ }
+ if data != "" {
+ t.Errorf("Expected data to be empty, got %q", data)
+ }
+ })
+
+ // Test not a pipe
+ t.Run("NotAPipe", func(t *testing.T) {
+ reader := bytes.NewBufferString("data that should be ignored")
+
+ data, hasPiped := mockCheckStdinPipe(reader, false)
+
+ if hasPiped {
+ t.Error("Expected hasPiped to be false, got true")
+ }
+ if data != "" {
+ t.Errorf("Expected data to be empty, got %q", data)
+ }
+ })
+} \ No newline at end of file