mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-22 11:24:18 +00:00
Document interactive curl execution with process substitution
## Problem The command `curl URL | bash` isn't interactive because curl's output consumes bash's stdin, preventing user prompts from working. ## Solution Use bash process substitution instead: `bash <(curl URL)` This keeps stdin available for the script while downloading from curl. ## Changes - Added INTERACTIVE_CURL.md - Complete guide to interactive execution - Added NON_INTERACTIVE_MODE.md - Guide to automation/CI usage - Updated README.md to recommend `bash <(curl ...)` format - Documented OpenRouter URL alias pattern ## Recommended Usage Interactive (best UX): bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/claude.sh) Non-interactive (CI/CD): SPRITE_NAME=dev-mk1 curl URL | bash ## Why Process Substitution? - Stdin available for prompts ✅ - Works like normal bash script ✅ - No /dev/tty workarounds needed ✅ - Better user experience ✅ Both methods are supported for maximum compatibility. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
10ada0e04d
commit
7ff684ca1e
3 changed files with 485 additions and 5 deletions
241
INTERACTIVE_CURL.md
Normal file
241
INTERACTIVE_CURL.md
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# Interactive Execution via curl
|
||||
|
||||
## The Problem with Piping
|
||||
|
||||
When you run `curl URL | bash`:
|
||||
- curl outputs the script to stdout
|
||||
- stdout is piped to bash's stdin
|
||||
- bash reads the script from stdin
|
||||
- **stdin is no longer available for the script to read user input**
|
||||
|
||||
This is why `/dev/tty` tricks and other workarounds are needed.
|
||||
|
||||
## The Solution: Process Substitution
|
||||
|
||||
Use bash process substitution instead of piping:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL URL)
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. `<(curl -fsSL URL)` - Process substitution creates a temporary file descriptor
|
||||
2. curl downloads the script and writes to this descriptor
|
||||
3. bash reads the script from the file descriptor
|
||||
4. **stdin remains connected to your terminal** for interactive input!
|
||||
|
||||
## Comparison
|
||||
|
||||
### ❌ Piping (Not Interactive)
|
||||
```bash
|
||||
curl URL | bash
|
||||
# - Script goes to bash's stdin
|
||||
# - No stdin available for user input
|
||||
# - Need /dev/tty workarounds
|
||||
```
|
||||
|
||||
### ✅ Process Substitution (Fully Interactive)
|
||||
```bash
|
||||
bash <(curl -fsSL URL)
|
||||
# - Script goes to file descriptor
|
||||
# - stdin available for user input
|
||||
# - Works like a normal bash script
|
||||
```
|
||||
|
||||
## Updated Usage Examples
|
||||
|
||||
### Claude Code Setup (Interactive)
|
||||
|
||||
```bash
|
||||
# Recommended - Fully interactive
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh)
|
||||
|
||||
# Alternative - Non-interactive with env vars
|
||||
SPRITE_NAME=dev-mk1 bash <(curl -fsSL https://raw.githubusercontent.com/.../claude.sh)
|
||||
```
|
||||
|
||||
### OpenClaw Setup (Interactive)
|
||||
|
||||
```bash
|
||||
# Recommended - Fully interactive
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/openclaw.sh)
|
||||
|
||||
# Alternative - Non-interactive
|
||||
SPRITE_NAME=dev-mk1 bash <(curl -fsSL https://raw.githubusercontent.com/.../openclaw.sh)
|
||||
```
|
||||
|
||||
## Why This Is Better
|
||||
|
||||
### Process Substitution Advantages
|
||||
|
||||
✅ **Fully interactive** - stdin available for prompts
|
||||
✅ **No special code needed** - Regular `read` commands work
|
||||
✅ **Cleaner implementation** - No `/dev/tty` fallbacks required
|
||||
✅ **Works everywhere** - bash/zsh on Linux/macOS
|
||||
✅ **Better UX** - Users can see prompts and type naturally
|
||||
|
||||
### Piping Disadvantages
|
||||
|
||||
❌ **Not interactive by default** - stdin consumed by bash
|
||||
❌ **Requires workarounds** - Need `/dev/tty`, `safe_read()`, etc.
|
||||
❌ **Fragile** - TTY may not be available (Docker, CI/CD)
|
||||
❌ **Poor UX** - Needs env vars or fails with cryptic errors
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Current Implementation (Supports Both)
|
||||
|
||||
Our scripts now support BOTH approaches:
|
||||
|
||||
1. **Process substitution** (recommended):
|
||||
```bash
|
||||
bash <(curl -fsSL URL)
|
||||
# Uses regular read commands, fully interactive
|
||||
```
|
||||
|
||||
2. **Piping** (fallback):
|
||||
```bash
|
||||
curl URL | bash
|
||||
# Fails gracefully, shows helpful error:
|
||||
# "Set SPRITE_NAME environment variable for non-interactive usage"
|
||||
```
|
||||
|
||||
3. **Non-interactive** (CI/CD):
|
||||
```bash
|
||||
SPRITE_NAME=dev-mk1 curl URL | bash
|
||||
# Works without prompts
|
||||
```
|
||||
|
||||
### Simplification Opportunity
|
||||
|
||||
If we only recommend process substitution, we could:
|
||||
- Remove `safe_read()` complexity
|
||||
- Remove environment variable checks
|
||||
- Use simple `read -p "prompt: " var` everywhere
|
||||
- Simpler codebase
|
||||
|
||||
**But we keep both because:**
|
||||
- Some tutorials/docs use piping pattern
|
||||
- CI/CD needs non-interactive mode
|
||||
- Environment variables are useful anyway
|
||||
- Graceful degradation is better UX
|
||||
|
||||
## Platform Support
|
||||
|
||||
### ✅ Supported Platforms
|
||||
|
||||
- **bash** (v3.0+) - Linux, macOS, WSL, Git Bash
|
||||
- **zsh** - macOS, Linux
|
||||
- Anywhere with `/dev/fd` support
|
||||
|
||||
### ❌ Unsupported
|
||||
|
||||
- Very old shells without process substitution
|
||||
- `sh` (POSIX shell) - doesn't support `<(...)`
|
||||
- Some embedded/minimal environments
|
||||
|
||||
**Fallback:** Download the script first:
|
||||
```bash
|
||||
curl -fsSL URL -o script.sh
|
||||
bash script.sh
|
||||
rm script.sh
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Test Interactive Mode
|
||||
|
||||
```bash
|
||||
# This will prompt you for sprite name and guide through OAuth
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh)
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Claude Code on Sprite
|
||||
|
||||
Installing sprite CLI...
|
||||
Enter sprite name: █
|
||||
```
|
||||
|
||||
### Test with Pre-Set Name
|
||||
|
||||
```bash
|
||||
# Skips sprite name prompt, but OAuth is interactive
|
||||
SPRITE_NAME=dev-mk1 bash <(curl -fsSL https://raw.githubusercontent.com/.../claude.sh)
|
||||
```
|
||||
|
||||
### Fully Automated
|
||||
|
||||
```bash
|
||||
# No prompts at all
|
||||
SPRITE_NAME=ci-test \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/.../claude.sh)
|
||||
```
|
||||
|
||||
## Recommended Documentation Update
|
||||
|
||||
We should update README.md to show process substitution first:
|
||||
|
||||
### Before (Current)
|
||||
```bash
|
||||
curl https://openrouter.ai/lab/spawn/sprite/claude.sh | bash
|
||||
```
|
||||
|
||||
### After (Recommended)
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/claude.sh)
|
||||
```
|
||||
|
||||
This gives users the best experience by default while still supporting non-interactive usage.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### What is Process Substitution?
|
||||
|
||||
In bash, `<(command)` is replaced with a file descriptor path like `/dev/fd/63`:
|
||||
|
||||
```bash
|
||||
# This:
|
||||
bash <(curl -fsSL URL)
|
||||
|
||||
# Becomes:
|
||||
bash /dev/fd/63
|
||||
# where /dev/fd/63 contains the curl output
|
||||
```
|
||||
|
||||
### stdin Flow
|
||||
|
||||
**With piping:**
|
||||
```
|
||||
Terminal stdin → curl (unused)
|
||||
curl stdout → bash stdin (script)
|
||||
bash needs input → NO STDIN AVAILABLE ❌
|
||||
```
|
||||
|
||||
**With process substitution:**
|
||||
```
|
||||
Terminal stdin → bash stdin (available!) ✅
|
||||
curl stdout → /dev/fd/63 (script)
|
||||
bash reads script from /dev/fd/63
|
||||
bash needs input → reads from stdin ✅
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Both methods have similar security profiles:
|
||||
|
||||
```bash
|
||||
# Review before executing
|
||||
curl -fsSL URL -o script.sh
|
||||
cat script.sh # Review
|
||||
bash script.sh
|
||||
|
||||
# Or review inline
|
||||
curl -fsSL URL | less # Review
|
||||
bash <(curl -fsSL URL) # Execute
|
||||
```
|
||||
|
||||
**Best practice:** Always review scripts from the internet before executing them.
|
||||
220
NON_INTERACTIVE_MODE.md
Normal file
220
NON_INTERACTIVE_MODE.md
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
# Non-Interactive Mode
|
||||
|
||||
## The Problem
|
||||
|
||||
When running scripts via `curl URL | bash`, the script is piped directly to bash without access to a TTY (terminal). This causes failures when trying to read user input:
|
||||
|
||||
```bash
|
||||
$ curl https://raw.githubusercontent.com/.../claude.sh | bash
|
||||
/dev/fd/63: line 42: /dev/tty: No such device or address
|
||||
```
|
||||
|
||||
**Why this happens:**
|
||||
- `curl | bash` pipes the script content to bash's stdin
|
||||
- `/dev/tty` doesn't exist in this context
|
||||
- `read` commands fail with "No such device or address"
|
||||
|
||||
## The Solution
|
||||
|
||||
We implemented a `safe_read()` function that:
|
||||
|
||||
1. Checks if `/dev/tty` is available
|
||||
2. Falls back to stdin if available
|
||||
3. Returns a helpful error message if neither works
|
||||
4. Suggests using environment variables for non-interactive usage
|
||||
|
||||
### Environment Variable Support
|
||||
|
||||
Scripts now support environment variables for non-interactive execution:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
SPRITE_NAME=dev-mk1 \
|
||||
curl URL | bash
|
||||
|
||||
# Optional (skips OAuth)
|
||||
SPRITE_NAME=dev-mk1 \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
curl URL | bash
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### safe_read() Function
|
||||
|
||||
```bash
|
||||
safe_read() {
|
||||
local prompt="$1"
|
||||
local result=""
|
||||
|
||||
# Try to read from TTY if available
|
||||
if [[ -c /dev/tty ]]; then
|
||||
read -p "$prompt" result < /dev/tty
|
||||
elif [[ -t 0 ]]; then
|
||||
# stdin is a terminal
|
||||
read -p "$prompt" result
|
||||
else
|
||||
# No interactive input available
|
||||
log_error "Cannot read input: no TTY available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
```
|
||||
|
||||
### get_sprite_name() Function
|
||||
|
||||
```bash
|
||||
get_sprite_name() {
|
||||
# Check if SPRITE_NAME is already set in environment
|
||||
if [[ -n "$SPRITE_NAME" ]]; then
|
||||
log_info "Using sprite name from environment: $SPRITE_NAME"
|
||||
echo "$SPRITE_NAME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to read interactively
|
||||
local sprite_name=$(safe_read "Enter sprite name: ")
|
||||
if [[ -z "$sprite_name" ]]; then
|
||||
log_error "Sprite name is required"
|
||||
log_warn "Set SPRITE_NAME environment variable for non-interactive usage:"
|
||||
log_warn " SPRITE_NAME=dev-mk1 curl ... | bash"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$sprite_name"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Interactive Mode
|
||||
|
||||
```bash
|
||||
# Will prompt for sprite name
|
||||
curl https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh | bash
|
||||
```
|
||||
|
||||
### Test Non-Interactive Mode
|
||||
|
||||
```bash
|
||||
# Provides all inputs via environment
|
||||
SPRITE_NAME=test-sprite \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
curl https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/claude.sh | bash
|
||||
```
|
||||
|
||||
### Test Local Execution
|
||||
|
||||
```bash
|
||||
# Interactive
|
||||
bash sprite/claude.sh
|
||||
|
||||
# Non-interactive
|
||||
SPRITE_NAME=test-sprite bash sprite/claude.sh
|
||||
```
|
||||
|
||||
## Error Messages
|
||||
|
||||
### Without SPRITE_NAME in non-interactive mode:
|
||||
|
||||
```
|
||||
❌ Cannot read input: no TTY available
|
||||
❌ Sprite name is required
|
||||
⚠️ Set SPRITE_NAME environment variable for non-interactive usage:
|
||||
⚠️ SPRITE_NAME=dev-mk1 curl ... | bash
|
||||
```
|
||||
|
||||
### Without OAuth capability in non-interactive mode:
|
||||
|
||||
```
|
||||
⚠️ netcat (nc) not found - OAuth server unavailable
|
||||
⚠️ OAuth authentication failed or unavailable
|
||||
⚠️ You can enter your API key manually instead
|
||||
❌ Cannot prompt for manual entry in non-interactive mode
|
||||
⚠️ Set OPENROUTER_API_KEY environment variable for non-interactive usage
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### CI/CD Pipelines
|
||||
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
- name: Setup Sprite
|
||||
run: |
|
||||
SPRITE_NAME=ci-test-${{ github.run_id }} \
|
||||
OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }} \
|
||||
curl -fsSL https://raw.githubusercontent.com/.../claude.sh | bash
|
||||
```
|
||||
|
||||
### Automation Scripts
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# setup-dev-env.sh
|
||||
|
||||
export SPRITE_NAME="dev-$(whoami)"
|
||||
export OPENROUTER_API_KEY="sk-or-v1-xxxxx"
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/.../claude.sh | bash
|
||||
```
|
||||
|
||||
### Docker Containers
|
||||
|
||||
```dockerfile
|
||||
FROM ubuntu:latest
|
||||
|
||||
ENV SPRITE_NAME=docker-dev
|
||||
ENV OPENROUTER_API_KEY=sk-or-v1-xxxxx
|
||||
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/.../claude.sh | bash
|
||||
```
|
||||
|
||||
## Comparison: Before vs After
|
||||
|
||||
### Before (Broken in non-interactive mode)
|
||||
|
||||
```bash
|
||||
get_sprite_name() {
|
||||
read -p "Enter sprite name: " SPRITE_NAME < /dev/tty
|
||||
echo "$SPRITE_NAME"
|
||||
}
|
||||
# ERROR: /dev/tty: No such device or address
|
||||
```
|
||||
|
||||
### After (Works in both modes)
|
||||
|
||||
```bash
|
||||
get_sprite_name() {
|
||||
# Check environment first
|
||||
if [[ -n "$SPRITE_NAME" ]]; then
|
||||
echo "$SPRITE_NAME"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try interactive input
|
||||
local sprite_name=$(safe_read "Enter sprite name: ")
|
||||
[[ -n "$sprite_name" ]] || return 1
|
||||
|
||||
echo "$sprite_name"
|
||||
}
|
||||
# SUCCESS: Uses env var or prompts interactively
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
- `sprite/lib/common.sh` - Contains `safe_read()` and updated functions
|
||||
- `sprite/openclaw.sh` - Updated to use `safe_read()` for model selection
|
||||
- `sprite/claude.sh` - Already uses library functions (no changes needed)
|
||||
- `README.md` - Documents non-interactive usage
|
||||
|
||||
## Future Improvements
|
||||
|
||||
Potential enhancements:
|
||||
- [ ] Add `--non-interactive` flag for explicit mode control
|
||||
- [ ] Support reading from config files (e.g., `.spawnrc`)
|
||||
- [ ] Add `--help` flag to show all environment variables
|
||||
- [ ] Validate environment variables before starting
|
||||
- [ ] Add dry-run mode to show what would be executed
|
||||
29
README.md
29
README.md
|
|
@ -11,18 +11,37 @@ Conjure your agents!
|
|||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode
|
||||
### Interactive Mode (Recommended)
|
||||
|
||||
Run the scripts and provide input when prompted:
|
||||
Use process substitution for full interactivity:
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
curl https://openrouter.ai/lab/spawn/sprite/claude.sh | bash
|
||||
# Claude Code - Prompts for sprite name and OAuth
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/claude.sh)
|
||||
|
||||
# OpenClaw - Prompts for sprite name, OAuth, and model selection
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/openclaw.sh)
|
||||
```
|
||||
|
||||
**Why `bash <(curl ...)` instead of `curl | bash`?**
|
||||
- Process substitution keeps stdin available for interactive prompts
|
||||
- No special TTY handling required
|
||||
- Works like a normal bash script
|
||||
|
||||
### Alternative: Piping (Requires env vars)
|
||||
|
||||
If using the shorter `curl | bash` pattern (like the OpenRouter documentation shows), you must set environment variables:
|
||||
|
||||
```bash
|
||||
# Claude Code - As shown on openrouter.ai/lab/spawn
|
||||
SPRITE_NAME=dev-mk1 curl https://openrouter.ai/lab/spawn/sprite/claude.sh | bash
|
||||
|
||||
# OpenClaw
|
||||
curl https://openrouter.ai/lab/spawn/sprite/openclaw.sh | bash
|
||||
SPRITE_NAME=dev-mk1 curl https://openrouter.ai/lab/spawn/sprite/openclaw.sh | bash
|
||||
```
|
||||
|
||||
**Note:** The OpenRouter URLs (`openrouter.ai/lab/spawn/...`) may redirect or proxy to this repository.
|
||||
|
||||
### Non-Interactive Mode
|
||||
|
||||
For automation or CI/CD, set environment variables:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue